use std::{
    borrow::Cow,
    io::{BufRead, BufReader, Read},
    process::{Command, Stdio},
    sync::mpsc::{Receiver, Sender},
};

type R<T> = std::result::Result<T, ()>;

fn printer_thrd(msg_recv: Receiver<Message>, mut printer: impl rustyline::ExternalPrinter) {
    while let Ok(mut msg) = msg_recv.recv() {
        if !msg.body.ends_with('\n') {
            msg.body.push('\n');
        }

        let _ = printer.print(format!("[{}] {}", msg.topic, msg.body));
    }
}

fn relay_reader<R: Read>(topic: &str, reader: BufReader<R>, msg_send: Sender<Message>) {
    for line in reader.lines() {
        let line = match line {
            Ok(line) => line,
            Err(err) => {
                let _ = msg_send.send(Message {
                    topic: "cmdg".into(),
                    body: format!("failed to readline: {err}"),
                });
                continue;
            }
        };

        let _ = msg_send.send(Message {
            topic: Cow::Owned(topic.to_owned()),
            body: line,
        });
    }
}

fn main() -> R<()> {
    let mut rl = rustyline::DefaultEditor::new()
        .map_err(|err| eprintln!("failed to create editor: {err}"))?;

    let (msg_send, msg_recv) = std::sync::mpsc::channel();

    let printer = rl
        .create_external_printer()
        .map_err(|err| eprintln!("failed to create printer: {err}"))?;

    let _ = std::thread::spawn(|| printer_thrd(msg_recv, printer));

    let mut cmds: Vec<CmdHandle> = vec![];

    loop {
        let line = match rl.readline(&get_prompt()) {
            Ok(line) => line,
            Err(rustyline::error::ReadlineError::Eof) => break,
            Err(err) => {
                let _ = msg_send.send(cmdg_message(err.to_string()));
                break;
            }
        };

        let xs: Vec<_> = shlex::Shlex::new(&line).collect();

        let [cmd, args @ ..] = xs.as_slice() else {
            continue;
        };

        match cmd.as_str() {
            "exit" => break,
            "stat" => {
                if args.is_empty() {
                    for cmd in &cmds {
                        let _ = msg_send.send(cmdg_message(cmd.cmd.to_string()));
                    }
                }
            }
            "cd" => {
                if args.len() != 1 {
                    let _ = msg_send.send(Message {
                        topic: "cd".into(),
                        body: format!("cd: too many args"),
                    });
                    continue;
                }

                if let Err(err) = std::env::set_current_dir(&args[0]) {
                    let _ = msg_send.send(Message {
                        topic: "cd".into(),
                        body: err.to_string(),
                    });
                }
            }
            _ => {
                spawn_cmd(&msg_send, cmd, args, &mut cmds);
                let _ = rl.add_history_entry(line);
                continue;
            }
        }
    }

    Ok(())
}

fn get_prompt() -> String {
    let pwd = std::env::current_dir().unwrap();
    let prompt = format!("{} >>> ", pwd.display());
    prompt
}

fn cmdg_message(body: String) -> Message {
    Message {
        topic: "cmdg".into(),
        body,
    }
}

struct CmdHandle {
    cmd: String,
    args: Vec<String>,
}

fn spawn_cmd(msg_send: &Sender<Message>, cmd: &String, args: &[String], cmds: &mut Vec<CmdHandle>) {
    let mut child = match Command::new(cmd)
        .args(args)
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
    {
        Ok(child) => child,
        Err(err) => {
            let _ = msg_send.send(cmdg_message(err.to_string()));
            return;
        }
    };

    std::thread::spawn({
        let stdout = child.stdout.take().unwrap();

        let tx = msg_send.clone();
        let cmd = cmd.clone();
        move || relay_reader(&format!("+{}", cmd), BufReader::new(stdout), tx)
    });

    std::thread::spawn({
        let stderr = child.stderr.take().unwrap();

        let msg_send = msg_send.clone();
        let cmd = cmd.clone();
        move || relay_reader(&format!("-{}", cmd), BufReader::new(stderr), msg_send)
    });

    std::thread::spawn({
        let msg_send = msg_send.clone();
        let cmd = cmd.clone();

        move || {
            let status = match child.wait() {
                Ok(ok) => ok,
                Err(err) => {
                    let _ = msg_send.send(cmdg_message(err.to_string()));
                    return;
                }
            };

            let message = if !status.success() {
                cmdg_message(format!("{cmd} exit with {:?}", status.code()))
            } else {
                cmdg_message(format!("{cmd} exit"))
            };

            let _ = msg_send.send(message);
        }
    });

    cmds.push(CmdHandle {
        cmd: cmd.clone(),
        args: args.to_vec(),
    });
}

struct Message {
    topic: Cow<'static, str>,
    body: String,
}
